﻿// Copyright (c) MudBlazor 2021
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics.CodeAnalysis;

namespace MudBlazor.Docs.Compiler;

#nullable enable
/// <summary>
/// Represents a writer for generated API documentation.
/// </summary>
public partial class ApiDocumentationWriter(string filePath) : StreamWriter(File.Create(filePath))
{
    /// <summary>
    /// Creates a new instance with types and the default output path.
    /// </summary>
    public ApiDocumentationWriter() : this(Paths.ApiDocumentationFilePath)
    {
    }

    /// <summary>
    /// Indents generated code to be more readable.
    /// </summary>
    /// <remarks>
    /// Defaults to <c>true</c>.  When <c>false</c>, no code will be indented, which saves space but is less readable.
    /// </remarks>
    public bool EnableIndentation { get; set; } = true;

    /// <summary>
    /// The current indentation level.
    /// </summary>
    public int IndentLevel { get; set; }

    /// <summary>
    /// Writes the copyright boilerplate.
    /// </summary>
    public void WriteHeader()
    {
        WriteLine($"// Copyright (c) MudBlazor {DateTime.Now.Year}");
        WriteLine("// MudBlazor licenses this file to you under the MIT license.");
        WriteLine("// See the LICENSE file in the project root for more information.");
        WriteLine();
        WriteLine("//-----------------------------------------------------------------------");
        WriteLine("// Generated by MudBlazor.Docs.Compiler.ApiDocumentationWriter");
        WriteLine("// Any changes to this file will be overwritten on build");
        WriteLine("// <auto-generated />");
        WriteLine("//-----------------------------------------------------------------------");
        WriteLine();
        WriteLine("using System.CodeDom.Compiler;");
        WriteLine();
        WriteLine("namespace MudBlazor.Docs.Models;");
        WriteLine();
    }

    /// <summary>
    /// Writes the start of the ApiDocumentation partial class.
    /// </summary>
    public void WriteClassStart()
    {
        WriteLine("/// <summary>");
        WriteLine("/// Represents all of the XML documentation for public-facing classes.");
        WriteLine("/// </summary>");
        WriteLine($"[GeneratedCodeAttribute(\"MudBlazor.Docs.Compiler\", \"{typeof(ApiDocumentationWriter).Assembly.GetName().Version}\")]");
        WriteLine("public static partial class ApiDocumentation");
        WriteLine("{");
        Indent();
    }

    /// <summary>
    /// Writes the end of the ApiDocumentation partial class.
    /// </summary>
    public void WriteClassEnd()
    {
        Outdent();
        WriteLine("}");
    }

    /// <summary>
    /// Writes a series of tabs to indent the line.
    /// </summary>
    public void WriteIndent()
    {
        if (!EnableIndentation)
        {
            return;
        }

        for (var index = 0; index < IndentLevel; index++)
        {
            Write("\t");
        }
    }

    /// <summary>
    /// Writes the start of the ApiDocumentation constructor.
    /// </summary>
    public void WriteConstructorStart()
    {
        WriteLineIndented("static ApiDocumentation()");
        WriteLineIndented("{");
        Indent();
    }

    /// <summary>
    /// Writes text with the current indentation level.
    /// </summary>
    /// <param name="text">The text to write.</param>
    public void WriteIndented(string text)
    {
        WriteIndent();
        Write(text);
    }

    /// <summary>
    /// Writes text with the current indentation level, and ends the line.
    /// </summary>
    /// <param name="text">The text to write.</param>
    public void WriteLineIndented(string text)
    {
        WriteIndented(text);
        WriteLine();
    }

    /// <summary>
    /// Writes the end of the ApiDocumentation constructor.
    /// </summary>
    public void WriteConstructorEnd()
    {
        Outdent();
        WriteLineIndented("}");
    }

    /// <summary>
    /// Writes the end of the ApiDocumentation class.
    /// </summary>
    public void WriteApiDocumentationClassEnd()
    {
        Outdent();
        WriteLine("}");
    }

    /// <summary>
    /// Increases the indentation level.
    /// </summary>
    public void Indent()
    {
        IndentLevel++;
    }

    /// <summary>
    /// Decreases the indentation level.
    /// </summary>
    public void Outdent()
    {
        IndentLevel--;
    }

    /// <summary>
    /// Formats a string for use in C# code.
    /// </summary>
    /// <param name="code"></param>
    [return: NotNullIfNotNull(nameof(code))]
    public static string? Escape(string? code) => code?.Replace("\"", "\"\"");

    /// <summary>
    /// Writes the category for the member.
    /// </summary>
    /// <param name="category">The category (derived from <see cref="CategoryAttribute"/>).</param>
    public void WriteCategory(string? category)
    {
        if (!string.IsNullOrEmpty(category))
        {
            Write($"Category = \"{category}\", ");
        }
    }

    /// <summary>
    /// Writes the category order.
    /// </summary>
    /// <param name="order">The category order (derived from <see cref="CategoryAttribute"/>).</param>
    public void WriteOrder(int? order)
    {
        // Wirte if the order is present, but not int.MaxValue  (which is the default)
        if (order.HasValue && order.Value != int.MaxValue)
        {
            Write($"Order = {order}, ");
        }
    }

    /// <summary>
    /// Serializes an XML summary for a member.
    /// </summary>
    /// <param name="summary"></param>
    public void WriteSummary(string? summary)
    {
        if (!string.IsNullOrEmpty(summary))
        {
            Write($"Summary = @\"{Escape(summary)}\", ");
        }
    }

    /// <summary>
    /// Serializes an XML summary for a member.
    /// </summary>
    /// <param name="summary"></param>
    public void WriteSummaryIndented(string? summary)
    {
        if (!string.IsNullOrEmpty(summary))
        {
            WriteLineIndented($"Summary = @\"{Escape(summary)}\", ");
        }
    }

    /// <summary>
    /// Serializes an XML remarks for a member.
    /// </summary>
    /// <param name="remarks"></param>
    public void WriteRemarks(string? remarks)
    {
        if (!string.IsNullOrEmpty(remarks))
        {
            Write($"Remarks = @\"{Escape(remarks)}\", ");
        }
    }

    /// <summary>
    /// Serializes an XML remarks for a member.
    /// </summary>
    /// <param name="remarks"></param>
    public void WriteLineRemarks(string? remarks)
    {
        if (!string.IsNullOrEmpty(remarks))
        {
            WriteLine($"Remarks = @\"{Escape(remarks)}\", ");
        }
    }

    /// <summary>
    /// Serializes the XML remarks for a member.
    /// </summary>
    /// <param name="remarks"></param>
    public void WriteRemarksIndented(string? remarks)
    {
        if (!string.IsNullOrEmpty(remarks))
        {
            WriteLineIndented($"Remarks = @\"{Escape(remarks)}\", ");
        }
    }

    /// <summary>
    /// Serializes the XML remarks for a method return value.
    /// </summary>
    /// <param name="returns">The XML docs for the method's return value.</param>
    public void WriteReturns(string? returns)
    {
        if (!string.IsNullOrEmpty(returns))
        {
            Write($"Returns = @\"{Escape(returns)}\", ");
        }
    }

    /// <summary>
    /// Serializes all the specified types.
    /// </summary>
    /// <param name="types">The types to serialize.</param>
    public void WriteTypes(IDictionary<string, DocumentedType> types)
    {
        WriteLineIndented("// Build all of the documented types");
        WriteLineIndented($"Types = new()");
        WriteLineIndented("{");
        Indent();

        foreach (var type in types)
        {
            WriteType(type.Value);
        }

        Outdent();
        WriteLineIndented("};");

        WriteLine();
    }

    /// <summary>
    /// Links all properties to their declaring types.
    /// </summary>
    public void LinkDocumentedTypes(IDictionary<string, DocumentedProperty> properties)
    {
        WriteLineIndented("// Link properties to their declaring types");

        foreach (var property in properties)
        {
            if (property.Value.DeclaringDocumentedType != null)
            {
                // Link directly to a documented type
                WriteLineIndented($"Properties[\"{property.Key}\"].DeclaringType = Types[\"{property.Value.DeclaringDocumentedType.Key}\"];");
            }
            else
            {
                // For external .NET types like ComponentBase, just set the name
                WriteLineIndented($"Properties[\"{property.Key}\"].DeclaringTypeName = \"{property.Value.DeclaringType?.Name}\";");
            }
            if (property.Value.ChangeEvent != null)
            {
                WriteLineIndented($"Properties[\"{property.Key}\"].ChangeEvent = Events[\"{property.Value.ChangeEvent.Key}\"];");
            }
        }

        WriteLine();
    }

    /// <summary>
    /// Links all properties to their declaring types.
    /// </summary>
    public void LinkDocumentedTypes(IDictionary<string, DocumentedField> fields)
    {
        WriteLineIndented("// Link fields to their declaring types");

        foreach (var field in fields)
        {
            if (field.Value.DeclaringDocumentedType != null)
            {
                WriteLineIndented($"Fields[\"{field.Key}\"].DeclaringType = Types[\"{field.Value.DeclaringDocumentedType.Key}\"];");
            }
            else
            {
                // For external .NET types like ComponentBase, just set the name
                WriteLineIndented($"Fields[\"{field.Key}\"].DeclaringTypeName = \"{field.Value.DeclaringType?.Name}\";");
            }
        }

        WriteLine();
    }

    /// <summary>
    /// Links all events to their declaring types.
    /// </summary>
    public void LinkDocumentedTypes(IDictionary<string, DocumentedEvent> events)
    {
        WriteLineIndented("// Link events to their declaring types");

        foreach (var eventItem in events)
        {
            if (eventItem.Value.DeclaringDocumentedType != null)
            {
                WriteLineIndented($"Events[\"{eventItem.Key}\"].DeclaringType = Types[\"{eventItem.Value.DeclaringDocumentedType.Key}\"];");
            }
            else
            {
                // For external .NET types like ComponentBase, just set the name
                WriteLineIndented($"Events[\"{eventItem.Key}\"].DeclaringTypeName = \"{eventItem.Value.DeclaringType?.Name}\";");
            }
            if (eventItem.Value.Property != null)
            {
                WriteLineIndented($"Events[\"{eventItem.Key}\"].Property = Properties[\"{eventItem.Value.Property.Key}\"];");
            }
        }

        WriteLine();
    }

    /// <summary>
    /// Links all events to their declaring types.
    /// </summary>
    public void LinkDocumentedTypes(IDictionary<string, DocumentedMethod> methods)
    {
        WriteLineIndented("// Link methods to their declaring types");

        foreach (var method in methods)
        {
            if (method.Value.DeclaringDocumentedType != null)
            {
                WriteLineIndented($"Methods[\"{method.Key}\"].DeclaringType = Types[\"{method.Value.DeclaringDocumentedType.Key}\"];");
            }
            else
            {
                // For external .NET types like ComponentBase, just set the name
                WriteLineIndented($"Methods[\"{method.Key}\"].DeclaringTypeName = \"{method.Value.DeclaringType?.Name}\";");
            }
        }

        WriteLine();
    }

    /// <summary>
    /// Serializes the specified type.
    /// </summary>
    /// <param name="type">The type to serialize.</param>
    public void WriteType(DocumentedType type)
    {
        WriteIndented("{ ");
        Write($"\"{type.Key}\", new()");
        WriteLine(" {");
        Indent();
        WriteLineIndented($"Name = \"{type.Name}\", ");
        WriteLineIndented($"NameFriendly = \"{type.Type.GetFriendlyName()}\", ");
        WriteBaseTypeIndented(type.BaseType);
        WriteIsComponentIndented(type.Type.IsSubclassOf(typeof(MudComponentBase)));
        WriteSummaryIndented(type.Summary);
        WriteRemarksIndented(type.Remarks);
        WriteProperties(type);
        WriteGlobalSettings(type);
        WriteFields(type);
        WriteMethods(type);
        WriteEvents(type);
        Outdent();
        WriteIndented("}");
        WriteLine("},");
    }

    /// <summary>
    /// Serializes all documented events.
    /// </summary>
    /// <param name="events">The events to write.</param>
    public void WriteEvents(IDictionary<string, DocumentedEvent> events)
    {
        WriteLineIndented("// Build all of the documented events");
        WriteLineIndented($"Events = new()");
        WriteLineIndented("{");
        Indent();

        foreach (var documentedEvent in events)
        {
            WriteEvent(documentedEvent.Value);
        }

        Outdent();
        WriteLineIndented("};");

        WriteLine();
    }

    /// <summary>
    /// Serializes a documented event.
    /// </summary>
    /// <param name="documentedEvent">The event to serialize.</param>
    public void WriteEvent(DocumentedEvent documentedEvent)
    {
        WriteIndented("{ ");
        Write($"\"{documentedEvent.Key}\", new()");
        Write(" { ");
        Write($"Name = \"{documentedEvent.Name}\", ");
        Write($"TypeName = \"{documentedEvent.Type?.FullName}\", ");
        Write($"TypeFriendlyName = \"{documentedEvent.Type?.GetFriendlyName()}\", ");
        WriteCategory(documentedEvent.Category);
        WriteOrder(documentedEvent.Order);
        WriteIsParameter(documentedEvent.IsParameter);
        WriteIsProtected(documentedEvent.IsProtected);
        WriteSummary(documentedEvent.Summary);
        WriteRemarks(documentedEvent.Remarks);
        Write("}");
        WriteLine("},");
    }

    /// <summary>
    /// Serializes all documented fields.
    /// </summary>
    /// <param name="fields">The fields to write.</param>
    public void WriteFields(IDictionary<string, DocumentedField> fields)
    {
        WriteLineIndented("// Build all of the documented fields");
        WriteLineIndented($"Fields = new()");
        WriteLineIndented("{");
        Indent();

        foreach (var field in fields)
        {
            WriteField(field.Value);
        }

        Outdent();
        WriteLineIndented("};");

        WriteLine();
    }

    /// <summary>
    /// Serializes a documented field.
    /// </summary>
    /// <param name="field">The field to serialize.</param>
    public void WriteField(DocumentedField field)
    {
        WriteIndented("{ ");
        Write($"\"{field.Key}\", new()");
        Write(" { ");
        Write($"Name = \"{field.Name}\", ");
        Write($"TypeName = \"{field.Type?.FullName}\", ");
        Write($"TypeFriendlyName = \"{field.Type?.GetFriendlyName()}\", ");
        WriteCategory(field.Category);
        WriteIsProtected(field.IsProtected);
        WriteOrder(field.Order);
        WriteSummary(field.Summary);
        WriteRemarks(field.Remarks);
        Write("}");
        WriteLine("},");
    }

    /// <summary>
    /// Serializes all documented properties.
    /// </summary>
    /// <param name="properties">the properties to write.</param>
    public void WriteProperties(IDictionary<string, DocumentedProperty> properties)
    {
        WriteLineIndented("// Build all of the documented properties");
        WriteLineIndented("Properties = new()");
        WriteLineIndented("{");
        Indent();

        foreach (var property in properties)
        {
            WriteProperty(property.Value);
        }

        Outdent();
        WriteLineIndented("};");

        WriteLine();
    }

    /// <summary>
    /// Serializes a documented property.
    /// </summary>
    /// <param name="property">the property to serialize.</param>
    public void WriteProperty(DocumentedProperty property)
    {
        WriteIndented("{ ");
        Write($"\"{property.Key}\", new()");
        Write(" { ");
        Write($"Name = \"{property.Name}\", ");
        Write($"TypeName = \"{property.Type?.FullName}\", ");
        Write($"TypeFriendlyName = \"{property.Type?.GetFriendlyName()}\", ");
        WriteCategory(property.Category);
        WriteIsParameter(property.IsParameter);
        WriteIsProtected(property.IsProtected);
        WriteOrder(property.Order);
        WriteRemarks(property.Remarks);
        WriteSummary(property.Summary);
        Write("}");
        WriteLine("},");
    }

    /// <summary>
    /// Serializes the parameters of methods.
    /// </summary>
    public void WriteMethodParameters(List<DocumentedParameter> parameters)
    {
        if (parameters.Count == 0)
        {
            return;
        }

        WriteLine("Parameters = ");

        Indent();
        WriteLineIndented("[");
        Indent();

        foreach (var parameter in parameters)
        {
            WriteMethodParameter(parameter);
        }

        Outdent();
        WriteLineIndented("],");
        Outdent();
        WriteIndent();
    }

    /// <summary>
    /// Serializes the specified properties.
    /// </summary>
    /// <param name="type">The type containing the properties.</param>
    public void WriteProperties(DocumentedType type)
    {
        /* Example:
         
            Properties = { 
				{ "Type.JavaScriptListenerId", Properties["Type.JavaScriptListenerId"], } },
				{ "Type.BrowserWindowSize", Properties["Type.BrowserWindowSize"], } },
				{ "Type.Breakpoint", Properties["Type.Breakpoint"],  } },
				{ "Type.IsImmediate", Properties["Type.IsImmediate"],  } },
            },
          
         */

        // Anything to do?
        if (type.Properties.Count == 0)
        {
            return;
        }

        WriteLineIndented("Properties = { ");
        Indent();

        foreach (var pair in type.Properties)
        {
            WriteTypeProperty(pair.Value);
        }

        Outdent();
        WriteLineIndented("},");
    }

    /// <summary>
    /// Serializes the specified MudGlobal settings.
    /// </summary>
    /// <param name="type">The type containing the settings.</param>
    public void WriteGlobalSettings(DocumentedType type)
    {
        /* Example:
         
            GlobalSettings = { 
				{ "JavaScriptListenerId", new() { Type = "Guid", Summary = "Gets the ID of the JavaScript listener.",  } },
				{ "BrowserWindowSize", new() { Type = "BrowserWindowSize", Summary = "Gets the browser window size.",  } },
				{ "Breakpoint", new() { Type = "Breakpoint", Summary = "Gets the breakpoint associated with the browser size.",  } },
				{ "IsImmediate", new() { Type = "Boolean",  } },
            },
          
         */

        // Anything to do?
        if (type.GlobalSettings.Count == 0)
        {
            return;
        }

        WriteLineIndented("GlobalSettings = { ");
        Indent();

        foreach (var property in type.GlobalSettings)
        {
            WriteTypeProperty(property.Value);
        }

        Outdent();
        WriteLineIndented("},");
    }

    /// <summary>
    /// Serializes the specified property.
    /// </summary>
    /// <param name="property">The property to serialize.</param>
    public void WriteTypeProperty(DocumentedProperty property)
    {
        WriteIndented("{ ");
        Write($"\"{property.Name}\", Properties[\"{property.Key}\"]");
        WriteLine(" },");
    }

    /// <summary>
    /// Serializes the specified event.
    /// </summary>
    /// <param name="eventItem">The event to serialize.</param>
    public void WriteTypeEvent(DocumentedEvent eventItem)
    {
        WriteIndented("{ ");
        Write($"\"{eventItem.Name}\", Events[\"{eventItem.Key}\"]");
        WriteLine(" },");
    }

    /// <summary>
    /// Serializes the specified field.
    /// </summary>
    /// <param name="field">The field to serialize.</param>
    public void WriteTypeField(DocumentedField field)
    {
        WriteIndented("{ ");
        Write($"\"{field.Name}\", Fields[\"{field.Key}\"]");
        WriteLine(" },");
    }

    /// <summary>
    /// Serializes the specified method.
    /// </summary>
    /// <param name="method">The method to serialize.</param>
    public void WriteTypeMethod(DocumentedMethod method)
    {
        WriteIndented("{ ");
        Write($"\"{method.Name}\", Methods[\"{method.Key}\"]");
        WriteLine(" },");
    }

    /// <summary>
    /// Serializes the specified methods.
    /// </summary>
    /// <param name="methods">The methods to serialize.</param>
    public void WriteMethods(IDictionary<string, DocumentedMethod> methods)
    {
        WriteLineIndented("// Build all of the documented methods");
        WriteLineIndented($"Methods = new()");
        WriteLineIndented("{");
        Indent();

        foreach (var method in methods)
        {
            WriteMethod(method.Value);
        }

        Outdent();
        WriteLineIndented("};");
        WriteLine();
    }

    /// <summary>
    /// Serializes the specified methods.
    /// </summary>
    /// <param name="type">The type containing the methods.</param>
    public void WriteMethods(DocumentedType type)
    {
        /* Example:

           Methods = { 
               { "SetValue", new() { Type = "Guid", Summary = "Gets the ID of the JavaScript listener.",  } },
               { "BrowserWindowSize", new() { Type = "BrowserWindowSize", Summary = "Gets the browser window size.",  } },
               { "Breakpoint", new() { Type = "Breakpoint", Summary = "Gets the breakpoint associated with the browser size.",  } },
               { "IsImmediate", new() { Type = "Boolean",  } },
           },

        */

        // Anything to do?
        if (type.Methods.Count == 0)
        {
            return;
        }

        WriteLineIndented("Methods = { ");
        Indent();

        foreach (var method in type.Methods)
        {
            WriteTypeMethod(method.Value);
        }

        Outdent();
        WriteLineIndented("},");
    }

    /// <summary>
    /// Serializes a documented method.
    /// </summary>
    /// <param name="method"></param>
    public void WriteMethod(DocumentedMethod method)
    {
        WriteIndented("{ ");
        Write($"\"{method.Key}\", new()");
        Write(" { ");
        Write($"Name = \"{method.Name}\", ");
        WriteReturnType(method);
        WriteCategory(method.Category);
        WriteIsProtected(method.IsProtected);
        WriteOrder(method.Order);
        WriteSummary(method.Summary);
        WriteRemarks(method.Remarks);
        WriteReturns(method.Returns);
        WriteMethodParameters(method.Parameters);
        Write("}");
        WriteLine("},");
    }

    /// <summary>
    /// Serializes the specified property.
    /// </summary>
    /// <param name="parameter">The property to serialize.</param>
    public void WriteMethodParameter(DocumentedParameter parameter)
    {
        WriteIndented("new() { ");
        Write($"Name = \"{parameter.Name}\", ");
        Write($"TypeName = \"{parameter.Type.FullName}\", ");
        Write($"TypeFriendlyName = \"{parameter.Type.GetFriendlyName()}\", ");
        WriteSummary(parameter.Summary);
        WriteLine("}, ");
    }

    /// <summary>
    /// Writes whether the type inherits from <see cref="MudComponentBase"/>.
    /// </summary>
    /// <param name="isComponent"></param>
    public void WriteIsComponentIndented(bool isComponent)
    {
        if (isComponent)
        {
            WriteIndent();
            WriteLine("IsComponent = true, ");
        }
    }

    /// <summary>
    /// Writes the type in which the property was declared, if it's another type.
    /// </summary>
    /// <param name="method">The property being described.</param>
    public void WriteReturnType(DocumentedMethod method)
    {
        Write($"TypeName = \"{Escape(method.Type?.Name)}\", ");
        Write($"TypeFriendlyName = \"{method.Type?.GetFriendlyName()}\", ");
    }

    /// <summary>
    /// Writes whether a property is a parameter.
    /// </summary>
    /// <param name="isParameter"></param>
    public void WriteIsParameter(bool isParameter)
    {
        if (isParameter)
        {
            Write("IsParameter = true, ");
        }
    }

    /// <summary>
    /// Writes whether a property is protected.
    /// </summary>
    /// <param name="isProtected"></param>
    public void WriteIsProtected(bool isProtected)
    {
        if (isProtected)
        {
            Write("IsProtected = true, ");
        }
    }

    /// <summary>
    /// Writes the name of the given base type.
    /// </summary>
    /// <param name="baseType"></param>
    public void WriteBaseTypeIndented(Type? baseType)
    {
        if (baseType is not null)
        {
            WriteLineIndented($"BaseTypeName = \"{baseType.Name}\", ");
        }
    }

    /// <summary>
    /// Serializes all fields for the specified type.
    /// </summary>
    /// <param name="type">The type being serialized.</param>
    public void WriteEvents(DocumentedType type)
    {
        if (type.Events.Count == 0)
        {
            return;
        }

        WriteLineIndented("Events = { ");
        Indent();

        foreach (var field in type.Events)
        {
            WriteTypeEvent(field.Value);
        }

        Outdent();
        WriteLineIndented("},");
    }

    /// <summary>
    /// Serializes all fields for the specified type.
    /// </summary>
    /// <param name="type">The type being serialized.</param>
    public void WriteFields(DocumentedType type)
    {
        if (type.Fields.Count == 0)
        {
            return;
        }

        WriteLineIndented("Fields = { ");
        Indent();

        foreach (var field in type.Fields)
        {
            WriteTypeField(field.Value);
        }

        Outdent();
        WriteLineIndented("},");
    }
}
